{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d8bfd1e7",
   "metadata": {},
   "source": [
    "# RunnableBranch\n",
    "\n",
    "`RunnableBranch` 는 입력에 따라 동적으로 로직을 라우팅할 수 있는 강력한 도구입니다. 이를 통해 개발자는 **입력 데이터의 특성에 기반하여 다양한 처리 경로를 유연하게 정의** 할 수 있습니다.\n",
    "\n",
    "`RunnableBranch` 는 복잡한 의사 결정 트리를 간단하고 직관적인 방식으로 구현할 수 있도록 도와줍니다. 이는 코드의 가독성과 유지보수성을 크게 향상시키며, 로직의 모듈화와 재사용성을 촉진합니다.\n",
    "\n",
    "또한, `RunnableBranch` 는 런타임에 동적으로 분기 조건을 평가하고 적절한 처리 루틴을 선택할 수 있어, 시스템의 적응력과 확장성을 높여줍니다.\n",
    "\n",
    "이러한 특징들로 인해 RunnableBranch는 다양한 도메인에서 활용될 수 있으며, 특히 입력 데이터의 다양성과 변동성이 큰 애플리케이션 개발에 매우 유용합니다. `RunnableBranch` 를 효과적으로 활용하면 코드의 복잡성을 줄이고, 시스템의 유연성과 성능을 향상시킬 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c79cc75",
   "metadata": {},
   "source": [
    "## 입력에 따른 동적 로직 라우팅\n",
    "\n",
    "LangChain Expression Language에서 라우팅을 수행하는 방법에 대해 다룹니다.\n",
    "\n",
    "라우팅을 통해 이전 단계의 출력이 다음 단계를 정의하는 비결정적 체인을 생성할 수 있습니다. 라우팅은 LLM과의 상호 작용에 구조와 일관성을 제공하는 데 도움이 됩니다.\n",
    "\n",
    "라우팅을 수행하는 방법에는 두 가지가 있습니다.\n",
    "\n",
    "1. `RunnableLambda` 에서 조건부로 실행 가능한 객체를 반환 (권장)\n",
    "2. `RunnableBranch` 사용\n",
    "\n",
    "두 가지 방법 모두 첫 번째 단계에서 입력 질문을 `수학`, `과학` 또는 `기타`에 대한 것으로 분류한 다음, 해당 프롬프트 체인으로 라우팅하는 두 단계 시퀀스를 사용하여 설명하겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "866ad1c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83b59c9c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH13-LCEL-Advanced\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f11dd8a3",
   "metadata": {},
   "source": [
    "## 간단한 예시\n",
    "\n",
    "먼저, 들어오는 질문이 `수학`, `과학`, 또는 `기타` 중 하나로 분류하는 Chain을 생성하겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8882ed62",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"주어진 사용자 질문을 `수학`, `과학`, 또는 `기타` 중 하나로 분류하세요. 한 단어 이상으로 응답하지 마세요.\n",
    "\n",
    "<question>\n",
    "{question}\n",
    "</question>\n",
    "\n",
    "Classification:\"\"\"\n",
    ")\n",
    "\n",
    "# 체인을 생성합니다.\n",
    "chain = (\n",
    "    prompt\n",
    "    | ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "    | StrOutputParser()  # 문자열 출력 파서를 사용합니다.\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ac93f5e",
   "metadata": {},
   "source": [
    "생성한 chain을 사용하여 질문을 분류합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7dd2920f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 체인을 호출합니다.\n",
    "chain.invoke({\"question\": \"2+2 는 무엇인가요?\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e55e7d65",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 체인을 호출합니다.\n",
    "chain.invoke({\"question\": \"작용 반작용의 법칙은 무엇인가요?\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa8d1e8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 체인을 호출합니다.\n",
    "chain.invoke({\"question\": \"Google은 어떤 회사인가요?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e900e44",
   "metadata": {},
   "source": [
    "이제 세 개의 하위 체인을 생성해 보겠습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "99226b51",
   "metadata": {},
   "outputs": [],
   "source": [
    "math_chain = (\n",
    "    PromptTemplate.from_template(\n",
    "        \"\"\"You are an expert in math. \\\n",
    "Always answer questions starting with \"깨봉선생님께서 말씀하시기를..\". \\\n",
    "Respond to the following question:\n",
    "\n",
    "Question: {question}\n",
    "Answer:\"\"\"\n",
    "    )\n",
    "    # OpenAI의 LLM을 사용합니다.\n",
    "    | ChatOpenAI(model=\"gpt-4o-mini\")\n",
    ")\n",
    "\n",
    "science_chain = (\n",
    "    PromptTemplate.from_template(\n",
    "        \"\"\"You are an expert in science. \\\n",
    "Always answer questions starting with \"아이작 뉴턴 선생님께서 말씀하시기를..\". \\\n",
    "Respond to the following question:\n",
    "\n",
    "Question: {question}\n",
    "Answer:\"\"\"\n",
    "    )\n",
    "    # OpenAI의 LLM을 사용합니다.\n",
    "    | ChatOpenAI(model=\"gpt-4o-mini\")\n",
    ")\n",
    "\n",
    "general_chain = (\n",
    "    PromptTemplate.from_template(\n",
    "        \"\"\"Respond to the following question concisely:\n",
    "\n",
    "Question: {question}\n",
    "Answer:\"\"\"\n",
    "    )\n",
    "    # OpenAI의 LLM을 사용합니다.\n",
    "    | ChatOpenAI(model=\"gpt-4o-mini\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1089f4c",
   "metadata": {},
   "source": [
    "## 사용자 정의 함수 사용하기\n",
    "\n",
    "LangChain 공식 도큐먼트에서 권장하는 방식이며, 서로 다른 출력 간의 라우팅을 위해 **사용자 정의 함수**를 `RunnableLambda` 로 래핑하여 활용할 수도 있습니다.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0905f48",
   "metadata": {},
   "outputs": [],
   "source": [
    "def route(info):\n",
    "    # 주제에 \"수학\"이 포함되어 있는 경우\n",
    "    if \"수학\" in info[\"topic\"].lower():\n",
    "        # datascience_chain을 반환\n",
    "        return math_chain\n",
    "    # 주제에 \"과학\"이 포함되어 있는 경우\n",
    "    elif \"과학\" in info[\"topic\"].lower():\n",
    "        # art_chain을 반환\n",
    "        return science_chain\n",
    "    # 그 외의 경우\n",
    "    else:\n",
    "        # general_chain을 반환\n",
    "        return general_chain"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b8c1975",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "from langchain_core.runnables import RunnableLambda\n",
    "\n",
    "full_chain = (\n",
    "    {\"topic\": chain, \"question\": itemgetter(\"question\")}\n",
    "    | RunnableLambda(\n",
    "        # 경로를 지정하는 함수를 인자로 전달합니다.\n",
    "        route\n",
    "    )\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77ab70c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 수학과 관련된 질문을 입력하여 체인을 호출합니다.\n",
    "full_chain.invoke({\"question\": \"미적분의 개념에 대해 말씀해 주세요.\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "768c3b9b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 과학과 관련된 질문을 입력하여 체인을 호출합니다.\n",
    "full_chain.invoke({\"question\": \"중력은 어떻게 작용하나요?\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5091b373",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 기타 질문을 입력하여 체인을 호출합니다.\n",
    "full_chain.invoke({\"question\": \"RAG(Retrieval Augmented Generation)은 무엇인가요?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b3a0a5f",
   "metadata": {},
   "source": [
    "## RunnableBranch\n",
    "\n",
    "`RunnableBranch`는 입력값에 따라 실행할 조건과 Runnable을 정의할 수 있는 특별한 유형의 `Runnable` 입니다.\n",
    "\n",
    "다만, 위에서 설명한 사용자 정의 함수로 구현할 수 없는 기능을 제공하지는 않으므로, 사용자 정의 함수를 사용하는 것이 좋습니다.\n",
    "\n",
    "**문법**\n",
    "\n",
    "- `RunnableBranch`는 (조건, Runnable) 쌍의 리스트와 기본 Runnable로 초기화됩니다.\n",
    "- 호출 시 전달된 입력값을 각 조건에 전달하여 분기를 선택합니다.\n",
    "- True로 평가되는 첫 번째 조건을 선택하고, 해당 조건에 해당하는 Runnable을 입력값과 함께 실행합니다.\n",
    "- 제공된 조건과 일치하는 것이 없으면 **기본 `Runnable`** 을 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3265e4dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "from langchain_core.runnables import RunnableBranch\n",
    "\n",
    "branch = RunnableBranch(\n",
    "    # 주제에 \"수학\"이 포함되어 있는지 확인하고, 포함되어 있다면 math_chain을 실행합니다.\n",
    "    (lambda x: \"수학\" in x[\"topic\"].lower(), math_chain),\n",
    "    # 주제에 \"과학\"이 포함되어 있는지 확인하고, 포함되어 있다면 science_chain을 실행합니다.\n",
    "    (lambda x: \"과학\" in x[\"topic\"].lower(), science_chain),\n",
    "    # 위의 조건에 해당하지 않는 경우 general_chain을 실행합니다.\n",
    "    general_chain,\n",
    ")\n",
    "# 주제와 질문을 입력받아 branch를 실행하는 전체 체인을 정의합니다.\n",
    "full_chain = (\n",
    "    {\"topic\": chain, \"question\": itemgetter(\"question\")} | branch | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39f587a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 전체 체인을 실행합니다.\n",
    "full_chain.invoke({\"question\": \"미적분의 개념에 대해 말씀해 주세요.\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d19d6a9b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 전체 체인을 실행합니다.\n",
    "full_chain.invoke({\"question\": \"중력 가속도는 어떻게 계산하나요?\"})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ffda11f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질문을 입력하여 전체 체인을 실행합니다.\n",
    "full_chain.invoke({\"question\": \"RAG(Retrieval Augmented Generation)은 무엇인가요?\"})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
